package com.lipaluma.test.db.internal.batch;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.DataSourceUtils;

import com.lipaluma.test.db.annotations.batch.Batch;
import com.lipaluma.test.db.exceptions.BatchException;
import com.lipaluma.test.db.exceptions.DBTestIllegalUseException;
import com.lipaluma.test.db.internal.Context;
import com.lipaluma.test.db.internal.DbUtils;

public class BatchUtils {
	private static Logger LOG = LoggerFactory.getLogger(BatchUtils.class);

	public static void doBatches(Batch.List batches) throws SQLException, IOException, FileNotFoundException {
		if(batches != null) {
			for (Batch batch : batches.value()) {
				doBatches(batch);
			}
		}
	}
	
	public static void doBatches(Batch batch) throws SQLException, IOException, FileNotFoundException {
		if (batch != null) {
			String[] scripts = batch.value();
			if (ArrayUtils.isEmpty(scripts)) {
				throw new DBTestIllegalUseException("Scripts must be defined in Batch Annotation.");
			} else {
				DataSource ds = getDataSource(batch.dataSource());
				for (String script : scripts) {
					long start = System.currentTimeMillis();
					executeBatch(ds, script, Param.convertAll(batch.params()));
					LOG.debug("Temps d'exécution du batch "+script+": "+(System.currentTimeMillis() - start)+" ms");
				}
			}
		}
	}

	/**
	 * execute a sql batch defined in the inputStream
	 * @param ds datasource
	 * @param inputStream stream that contains requests to launch
	 * @param params Param to replace in requests
	 * @throws SQLException
	 * @throws IOException
	 */
	public static void executeBatch(DataSource ds, InputStream inputStream, Param... params) throws SQLException, IOException {
		executeBatch(ds, new InputStreamReader(inputStream), params);
	}
	
	/**
	 * execute a sql batch defined in the file
	 * @param ds datasource
	 * @param filename file with sql queries to launch
	 * @param params params to replace
	 * @throws SQLException
	 * @throws IOException
	 */
	public static void executeBatch(DataSource ds, String filename, Param... params) throws SQLException, IOException {
		try {
			executeBatch(ds, getScriptInputStream(filename, Context.getTestedClass()), params);
		} catch (BatchException e) {
			throw new BatchException("Error on file : "+filename , e);
		}
	}

	/**
	 * execute a sql batch defined in the reader
	 * @param ds datasource
	 * @param reader the reader tahat contains the sql queries to launch
	 * @param params params to replace
	 * @throws SQLException
	 * @throws IOException
	 */
	public static void executeBatch(DataSource ds, Reader reader, Param... params) throws SQLException, IOException {
		Connection connection = DataSourceUtils.getConnection(ds);
		List<Instruction> instructions = mapInstructionsByLineNumber(reader, params);
		
		Statement statement = connection.createStatement();
		try {
			for (Instruction instruction : instructions) {
				try {
					statement.executeUpdate(instruction.instruction);
				} catch (SQLException e) {
					throw new BatchException("Error on line "+instruction.lineNumber, e);
				}
			}
		} finally {
			statement.close();
		}
	}

	private static List<Instruction> mapInstructionsByLineNumber(Reader reader, Param[] params) throws IOException {
		List<Instruction> instructions = new ArrayList<Instruction>();
		BufferedReader bufReader = new BufferedReader(reader);
		String line;
		int lineCounter = 1;
		int currentLine = lineCounter;
		StringBuilder currentInstruction = new StringBuilder();
		while ((line = bufReader.readLine()) != null) {
			if (StringUtils.isNotBlank(line) && !line.startsWith("--")) {
				if("".equals(currentInstruction.toString()))
					currentLine = lineCounter;
				else
					currentInstruction.append("\n");
				currentInstruction.append(line.trim());
				if(currentInstruction.toString().endsWith(";")) {
					instructions.add(constructInstruction(currentLine, currentInstruction, params));
					currentInstruction = new StringBuilder();
				}
			}
			lineCounter++;
		}
		if(StringUtils.isNotBlank(currentInstruction.toString()))
			instructions.add(constructInstruction(currentLine, currentInstruction, params));
			
		return instructions;
	}

	private static Instruction constructInstruction(int currentLine, StringBuilder currentInstruction, Param[] params) {
		String instruction = currentInstruction.toString();
		for (Param param : params) {
			instruction = instruction.replaceAll(param.getKey(), param.getValue());
		}
		return new Instruction(instruction, currentLine);
	}

	/**
	 * get the inputStream for the filename script
	 * @param scriptPath path of the file
	 * @param testClass the test class
	 * @return
	 * @throws FileNotFoundException
	 */
	public static InputStream getScriptInputStream(String scriptPath, Class<?> testClass) throws FileNotFoundException {
		if (scriptPath.startsWith("classpath:")) {
			String scriptName = scriptPath.substring("classpath:".length());
			InputStream inputStream = testClass.getResourceAsStream(scriptName);
			if (inputStream == null) {
				inputStream = ClassLoader.getSystemResourceAsStream(scriptName);
				if (inputStream == null)
					throw new FileNotFoundException("the script " + scriptName + " was not found on classpath.");
			}
			return inputStream;
		} else {
			return new FileInputStream(new File(scriptPath));
		}
	}

	
	private static class Instruction {
		public Instruction(String instruction, int lineNumber) {
			this.instruction = instruction;
			this.lineNumber = lineNumber;
		}
		private String instruction;
		private int lineNumber;
	}
	
	private static DataSource getDataSource(String dataSourceName) throws IOException {
		DataSource ds;
		if(dataSourceName.isEmpty())
			ds = DbUtils.getDataSource();
		else 
			ds = DbUtils.getDataSourceWithName(dataSourceName);
		return ds;
	}
}
